﻿/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

Author:       François PIETTE @ Balteau-NDT
Creation:     Feb 11, 2015
Description:  Demo showing how to automate Balteau NDT control Balteau-NDT
              applications using a TCP/IP connection.
              This source is the base class for all client classes.  
Version:      1.00
Legal issues: This software is Open Source and subject to "The MIT License (MIT)"
              Copyright (C) 2015 Balteau-NDT
              Rue Voie de Liège 12, 4681 Hermalle Ss Argenteau, Belgium
              http://www.balteau-ndt.com
 
              Permission is hereby granted, free of charge, to any person 
              obtaining a copy of this software and associated documentation
              files (the "Software"), to deal in the Software without 
              restriction, including without limitation the rights to use, 
              copy, modify, merge, publish, distribute, sublicense, and/or 
              sell copies of the Software, and to permit persons to whom 
              the Software is furnished to do so, subject to the following 
              conditions:

              The above copyright notice and this permission notice shall 
              be included in all copies or substantial portions of the Software.

              THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
              EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
              OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
              NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
              HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
              WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
              OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
              DEALINGS IN THE SOFTWARE.

History:


 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Collections;
using System.Text;
using Balteau;

namespace Balteau
{
    public class DisplayEventArgs : EventArgs
    {
        public DisplayEventArgs(string aMsg)
        {
            msg = aMsg;
        }
        public string msg { get; set; }
    }

    public class ConnectedEventArgs : EventArgs
    {
        public ConnectedEventArgs(int aErrCode, object aBalteauXRay)
        {
            errCode     = aErrCode;
            balteauXRay = (XRayRemoteClientTcp)aBalteauXRay;
        }
        public int errCode;
        public XRayRemoteClientTcp balteauXRay;
    }

    public class DisconnectedEventArgs : EventArgs
    {
        public DisconnectedEventArgs(int aErrCode, object aBalteauXRay)
        {
            errCode = aErrCode;
            balteauXRay = (XRayRemoteClientTcp)aBalteauXRay;
        }
        public int errCode;
        public XRayRemoteClientTcp balteauXRay;
    }

    public class DataAvailableEventArgs : EventArgs
    {
        public DataAvailableEventArgs(int aErrCode, object aBalteauXRay)
        {
            errCode     = aErrCode;
            balteauXRay = (XRayRemoteClientTcp)aBalteauXRay;
        }
        public int errCode;
        public XRayRemoteClientTcp balteauXRay;
    }

    public class CommandDoneEventArgs : EventArgs
    {
        public CommandDoneEventArgs(string aCmd, int aRespCode, string aRespMsg, object aBalteauXRay)
        {
            cmd         = aCmd;
            respCode    = aRespCode;
            respMsg     = aRespMsg;
            balteauXRay = (XRayRemoteClientTcp)aBalteauXRay;
        }
        public string cmd;
        public int    respCode;
        public string respMsg;
        public XRayRemoteClientTcp balteauXRay;
    }

    public delegate void DisplayEventHandler(object sender, DisplayEventArgs e);
    public delegate void ConnectedEventHandler(object sender, ConnectedEventArgs e);
    public delegate void DisconnectedEventHandler(object sender, DisconnectedEventArgs e);
    public delegate void DataAvailableEventHandler(object sender, DataAvailableEventArgs e);
    public delegate void CommandDoneEventHandler(object sender, CommandDoneEventArgs e);


    public class RemoteClientBaseTcp
    {
        public event DisplayEventHandler       Display;
        public event ConnectedEventHandler     Connected;
        public event DisconnectedEventHandler  Disconnected;
        public event DataAvailableEventHandler DataAvailable;
        public event CommandDoneEventHandler   CommandDone;

        public enum  ProtocolState            {tpsDisconnected,
                                               tpsConnecting,
                                               tpsConnected,
                                               tpsWaitingBanner,
                                               tpsReady,
                                               tpsWaitingResponse,
                                               tpsDisconnecting};
        protected Socket                       client            = null;
        public    string                       ipAddress         = "127.0.0.1";
        public    int                          port              = 2346;
        protected SynchronizationContext       uiContext         = null;
        protected RemoteClientCommandQueue     commandQueue      = new RemoteClientCommandQueue();
        protected RemoteClientCommand          commandExecuting  = null;
        protected StringBuilder                sb                = new StringBuilder();   // Received data string
        public    string                       response          = string.Empty;
        public    int                          notificationCode  = 911;
        protected const int                    receiveBufferSize = 1024;
        protected byte[]                       receiveBuffer     = new byte[receiveBufferSize];
        public    ProtocolState                state             = ProtocolState.tpsDisconnected;
        protected bool                         ignoreDisconnect  = false;
        public    bool                         connected         { get { return state != ProtocolState.tpsDisconnected; }}
        protected bool                         connecting        { get { return state == ProtocolState.tpsConnecting;   }}

        public RemoteClientBaseTcp()
        {
            // Asynchronous socket connection use a worker thread. Since we want to
            // simplify user programming and trigger events in the context of the
            // main thread (UI thread), we need a context.
            uiContext = SynchronizationContext.Current;
            DataAvailable += DataAvailableHandler;
        }

        protected virtual void TriggerDisplay(string Msg)
        {
            if (Display != null)
                Display(this, new DisplayEventArgs(Msg));
        }

        protected virtual void TriggerConnected(object state)
        {
            XRayRemoteClientTcp xRay = ((ConnectedEventArgs)state).balteauXRay;

            if (xRay.Connected != null)
                xRay.Connected(xRay, (ConnectedEventArgs)state);
        }

        protected virtual void TriggerDisconnected(object state)
        {
            if (ignoreDisconnect)
                return;

            XRayRemoteClientTcp xRay = ((DisconnectedEventArgs)state).balteauXRay;

            if (commandExecuting != null) {
                TriggerDisplay("Command \"" + commandExecuting.cmd + "\" was not terminated");
                commandExecuting = null;
            }
            if (commandQueue.Count() > 0) {
                TriggerDisplay(commandQueue.Count().ToString() + " queued commands deleted");
                commandQueue.Clear();
            }

            if (xRay.Disconnected != null)
                xRay.Disconnected(xRay, (DisconnectedEventArgs)state);
        }

        protected virtual void TriggerDataAvailable(object state)
        {
            XRayRemoteClientTcp xRay = ((DataAvailableEventArgs)state).balteauXRay;

            if (xRay.DataAvailable != null)
                xRay.DataAvailable(xRay, (DataAvailableEventArgs)state);
        }

        protected virtual void TriggerReady()
        {
        }

        protected virtual void TriggerCommandDone(string aCmd, int aRespCode, string aRespMsg)
        {
            if (CommandDone != null)
                CommandDone(this, new CommandDoneEventArgs(aCmd, aRespCode, aRespMsg, this));
        }

        protected virtual void SetState(ProtocolState newState)
        {
            state = newState;
        }

        protected void DataAvailableHandler(object sender, DataAvailableEventArgs e)
        {
            TriggerDisplay("Received: " + response);
            // Any valid response begins with a 3 digits number, then a space or a dash
            // and finally a text message.
            // When a dash is found, it is a continuation line.
            // When a space is found, it is the last line of a response.
            if (response.Length < 4) {
                TriggerDisplay("Ignoring to short response " + response);
                return;
            }
            if (!(Char.IsDigit(response[0]) &&
                  Char.IsDigit(response[1]) &&
                  Char.IsDigit(response[2]) &&
                  ((response[3] == ' ') || (response[3] == '-')))) {
                TriggerDisplay("Ignoring invalid response " + response);
                return;
            }

            int    respCode = Convert.ToInt32(response.Substring(0, 3));
            string respMsg  = response.Substring(4, response.Length - 4);

            if (response[3] == '-')
                ProcessContinuationLine(respCode, respMsg);
            else
                ProcessResponse(respCode, respMsg);
        }

        // We have receive a continuation line.
        protected virtual void ProcessContinuationLine(int respCode, string respMsg)
        {
            // Can be overriden in derived classes
        }

        protected virtual void ProcessNotification(string aParams)
        {
            // Can be overriden in derived classes
        }

        // We have receive a syntactically valid response.
        protected virtual void ProcessResponse(int respCode, string respMsg)
        {
            if (respCode == notificationCode) {
                // All responses with FNotificationCode (911) code are actually notifications.
                ProcessNotification(respMsg);
            } 
            else if (state == ProtocolState.tpsWaitingBanner) {
                if (respCode == 221) {
                    // This is a valid banner, we consider as connected at the app level.
                    SetState(ProtocolState.tpsReady);
                    TriggerReady();
                    TryToSend(); 
                }
                else {
                    // Invalid response code for a banner, disconnect.
                    TriggerDisplay("Invalid banner received. Disconnecting");
                    Disconnect();
                }
            }
            else if (state == ProtocolState.tpsWaitingResponse) {
                ProcessResponseOfCommand(respCode, respMsg);
            }
            else {
                TriggerDisplay("Ignoring unexpected data received");
                TryToSend();
            }
        }

        protected virtual void ProcessResponseOfCommand(int respCode, string respMsg)
        {
            TriggerDisplay("Response received for command \"" + 
                           commandExecuting.cmd + 
                           ((string.Equals(commandExecuting.parameters, ""))?"":" ") +
                           commandExecuting.parameters + "\"");
            TriggerCommandDone(commandExecuting.cmd, respCode, respMsg);
            commandExecuting = null;
            SetState(ProtocolState.tpsReady);
            TryToSend();
        }

        protected virtual bool TryToSend()
        {
            if (state != ProtocolState.tpsReady)
                return true;
            if (commandExecuting != null)
                return true;

            RemoteClientCommand obj = commandQueue.Extract();
            if (obj == null)
                return true;

            commandExecuting = obj;
            SetState(ProtocolState.tpsWaitingResponse);
            if (obj.parameters == "")
                client.Send(Encoding.ASCII.GetBytes(obj.cmd + "\r\n"));
            else
                client.Send(Encoding.ASCII.GetBytes(obj.cmd + " " + obj.parameters + "\r\n"));

            return true;
        }

        private static void ReceiveCallback(IAsyncResult ar)
        {
            try
            {
                XRayRemoteClientTcp xRay   = (XRayRemoteClientTcp)ar.AsyncState;
                Socket              client = xRay.client;

                // Read data from the remote device.
                int bytesRead = client.EndReceive(ar);

                if (bytesRead > 0) {
                    // Search for complete line.
                    int i = 1;
                    while (i < bytesRead) {
                        // Search the end of line marker (CRLF).
                        if ((xRay.receiveBuffer[i - 1] == 13) && (xRay.receiveBuffer[i] == 10)) {
                            // Copy received line without ending CRLF into response string.
                            xRay.sb.Append(Encoding.ASCII.GetString(xRay.receiveBuffer, 0, i - 1));
                            xRay.response = xRay.sb.ToString();
                            // Give data to the application.
                            // We must execute the event handler in the context of the main thread.
                            xRay.uiContext.Send(xRay.TriggerDataAvailable, new DataAvailableEventArgs(0, xRay));
                            xRay.sb.Clear();
                            // Move remaining data in front of receive buffer.
                            int j = i + 1;
                            int k = 0;
                            while (j < bytesRead)
                                xRay.receiveBuffer[k++] = xRay.receiveBuffer[j++];
                            bytesRead -= i + 1;
                            i = 1;
                        }
                        else
                            i++;
                    }
                    // Get the rest of the data.
                    client.BeginReceive(xRay.receiveBuffer,
                                        0,
                                        XRayRemoteClientTcp.receiveBufferSize,
                                        SocketFlags.None,
                                        new AsyncCallback(ReceiveCallback), xRay);
                }
                else {
                    // Connection closed.
                    xRay.SetState(ProtocolState.tpsDisconnected);

                    // Signal that the connection has been closed.
                    // We must execute the event handler in the context of the main thread.
                    xRay.uiContext.Send(xRay.TriggerDisconnected, new DisconnectedEventArgs(0, xRay));
                }
            }
            catch (Exception e) {
                Console.WriteLine(e.ToString());
            }
        }

        private static void ConnectCallback(IAsyncResult ar)
        {
            try {
                XRayRemoteClientTcp xRay   = (XRayRemoteClientTcp)ar.AsyncState;
                Socket              client = xRay.client;

                // Complete the connection.
                try {
                    client.EndConnect(ar);
                }
                catch (SocketException se) {
                    xRay.SetState(ProtocolState.tpsDisconnected);
                    // Signal that the connection has failed.
                    // We must execute the event handler in the context of the main thread.
                    xRay.uiContext.Send(xRay.TriggerConnected, new ConnectedEventArgs(se.ErrorCode, xRay));
                    return;
                }

                // We are connected and waiting for banner.
                xRay.SetState(ProtocolState.tpsWaitingBanner);

                // Signal that the connection has been made.
                // We must execute the event handler in the context of the main thread.
                xRay.uiContext.Send(xRay.TriggerConnected, new ConnectedEventArgs(0, xRay));

                // Begin receiving data from the remote device.
                client.BeginReceive(xRay.receiveBuffer, 
                                    0, 
                                    XRayRemoteClientTcp.receiveBufferSize,
                                    SocketFlags.None, 
                                    new AsyncCallback(ReceiveCallback), xRay);
            }
            catch (Exception e) {
                Console.WriteLine(e.ToString());
            }
        }


        private static void DisconnectCallback(IAsyncResult ar)
        {
            try {
                XRayRemoteClientTcp xRay   = (XRayRemoteClientTcp)ar.AsyncState;
                Socket              client = xRay.client;
                
                // Remember we are disconnected.
                xRay.SetState(ProtocolState.tpsDisconnected);
                
                // Complete the disconnection.
                try {
                    client.EndDisconnect(ar);
                }
                catch (SocketException se) {
                    // Signal that the disconnection has failed.
                    // We must execute the event handler in the context of the main thread.
                    xRay.uiContext.Send(xRay.TriggerDisconnected, new DisconnectedEventArgs(se.ErrorCode, xRay));
                    return;
                }
                // Disconnect OK. Do not trigger the event beacuse it is done from ReceiveCallback.
            }
            catch (Exception e) {
                Console.WriteLine(e.ToString());
            }
        }

        public virtual bool Connect()
        {
            // Create a TCP/IP socket.
            client = new Socket(AddressFamily.InterNetwork,
                                SocketType.Stream, 
                                ProtocolType.Tcp);
            // Connect to the remote endpoint.
            client.BeginConnect(new IPEndPoint(IPAddress.Parse(ipAddress), port),
                                new AsyncCallback(ConnectCallback), 
                                this);
            SetState(ProtocolState.tpsConnecting);
            return true;
        }

        public virtual bool Disconnect()
        {
            if (connecting || connected)
                client.BeginDisconnect(true, new AsyncCallback(DisconnectCallback), this);
            return true;
        }

        public virtual bool QueueCmd(string aCmd, string aParameters)
        {
            bool result = commandQueue.Add(aCmd, aParameters);
            TryToSend();
            return result;
        }
    }
}
